#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stdint.h>
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <vector>
#include <map>
#include <arpa/inet.h>
#include "stats.hpp"
#include "main.hpp"
#include "hub-server.hpp"

FILE *stats_file = 0;
static pthread_t xlln_thread_stats_file;

void GetStatsConnectedEntities(size_t *numOfConnectedInstances, size_t *numOfAdditionalSockets, size_t *numOfRecentlyIgnoredUnknownAddrs)
{
	pthread_mutex_lock(&xlln_mutex_packet_consume);
	
	for (uint32_t iRemoteSocket = 0; iRemoteSocket < xlln_remote_socket_info.size(); iRemoteSocket++) {
		REMOTE_SOCKET_INFO* &savedRemoteSocket = xlln_remote_socket_info.at(iRemoteSocket);
		switch (savedRemoteSocket->isCoreSocket) {
			case IS_CORE_SOCKET::Type::YES: {
				if (numOfConnectedInstances) {
					(*numOfConnectedInstances)++;
				}
				break;
			}
			case IS_CORE_SOCKET::Type::NO: {
				if (numOfAdditionalSockets) {
					(*numOfAdditionalSockets)++;
				}
				break;
			}
			default: {
				if (numOfRecentlyIgnoredUnknownAddrs) {
					(*numOfRecentlyIgnoredUnknownAddrs)++;
				}
				break;
			}
		}
	}
	
	pthread_mutex_unlock(&xlln_mutex_packet_consume);
}

// map<titleId|titleversion, count>
void GetStatsTitleVersions(std::map<uint64_t, size_t>& versions)
{
	pthread_mutex_lock(&xlln_mutex_packet_consume);
	
	for (uint32_t iRemoteSocket = 0; iRemoteSocket < xlln_remote_socket_info.size(); iRemoteSocket++) {
		REMOTE_SOCKET_INFO* &savedRemoteSocket = xlln_remote_socket_info.at(iRemoteSocket);
		if (savedRemoteSocket->isCoreSocket != IS_CORE_SOCKET::Type::YES) {
			continue;
		}
		uint64_t keyTitleIdVersion = ((uint64_t)savedRemoteSocket->titleId << 32) + savedRemoteSocket->titleVersion;
		auto const currentCountPair = versions.find(keyTitleIdVersion);
		size_t currentCount;
		if (currentCountPair == versions.end()) {
			currentCount = 1;
		}
		else {
			currentCount = currentCountPair->second + 1;
		}
		versions[keyTitleIdVersion] = currentCount;
	}
	
	pthread_mutex_unlock(&xlln_mutex_packet_consume);
}

// map<xllnVersion, count>
void GetStatsXllnVersion(std::map<uint32_t, size_t>& versions)
{
	pthread_mutex_lock(&xlln_mutex_packet_consume);
	
	for (uint32_t iRemoteSocket = 0; iRemoteSocket < xlln_remote_socket_info.size(); iRemoteSocket++) {
		REMOTE_SOCKET_INFO* &savedRemoteSocket = xlln_remote_socket_info.at(iRemoteSocket);
		if (savedRemoteSocket->isCoreSocket != IS_CORE_SOCKET::Type::YES) {
			continue;
		}
		auto const currentCountPair = versions.find(savedRemoteSocket->xllnVersion);
		size_t currentCount;
		if (currentCountPair == versions.end()) {
			currentCount = 1;
		}
		else {
			currentCount = currentCountPair->second + 1;
		}
		versions[savedRemoteSocket->xllnVersion] = currentCount;
	}
	
	pthread_mutex_unlock(&xlln_mutex_packet_consume);
}

void* ThreadStatsFile(void *arg)
{
	FILE *fpStats = (FILE*)arg;
	if (!fpStats) {
		return 0;
	}
	
	struct timespec timeoutTime;
	while (1) {
		clock_gettime(CLOCK_REALTIME, &timeoutTime);
		timeoutTime.tv_sec += 15;
		int resultTimedLock = pthread_mutex_timedlock(&xlln_mutex_exit_threads, &timeoutTime);
		if (resultTimedLock != ETIMEDOUT) {
			if (resultTimedLock == 0) {
				pthread_mutex_unlock(&xlln_mutex_exit_threads);
			}
			XLLN_DEBUG_LOG(XLLN_LOG_CONTEXT_XLIVELESSNESS | XLLN_LOG_LEVEL_DEBUG | XLLN_LOG_LEVEL_INFO
				, "Exiting %s."
				, __func__
			);
			break;
		}
		
		char timestampISO8601[21];
		struct tm tm;
		gmtime_r(&timeoutTime.tv_sec, &tm);
		strftime(timestampISO8601, 21, "%Y-%m-%dT%H:%M:%SZ", &tm);
		
		size_t numOfConnectedInstances = 0;
		size_t numOfAdditionalSockets = 0;
		size_t numOfRecentlyIgnoredUnknownAddrs = 0;
		GetStatsConnectedEntities(&numOfConnectedInstances, &numOfAdditionalSockets, &numOfRecentlyIgnoredUnknownAddrs);
		
		std::map<uint32_t, size_t> xllnVersions;
		GetStatsXllnVersion(xllnVersions);
		
		std::map<uint64_t, size_t> titleVersions;
		GetStatsTitleVersions(titleVersions);
		
		std::map<uint32_t, size_t> titles;
		for (auto const &titleVersion : titleVersions) {
			uint32_t titleId = (uint32_t)(titleVersion.first >> 32);
			auto const currentCountPair = titles.find(titleId);
			size_t currentCount;
			if (currentCountPair == titles.end()) {
				currentCount = 1;
			}
			else {
				currentCount = currentCountPair->second + 1;
			}
			titles[titleId] = currentCount;
		}
		
		fseek(fpStats, 0, SEEK_SET);
		
		fprintf(fpStats, "{\r\n\"DateTime\": \"%s\",\r\n\"NumOfXLLNInstances\": %zu,\r\n\"XLLNVersions\": [", timestampISO8601, numOfConnectedInstances);
		
		for (auto const &version : xllnVersions) {
			fprintf(fpStats, "\r\n{\"%hhu.%hhu.%hhu.%hhu\": %zu},", (uint8_t)(version.first >> 24), (uint8_t)((version.first >> 16) & 0xFF), (uint8_t)((version.first >> 8) & 0xFF), (uint8_t)(version.first & 0xFF), version.second);
		}
		if (xllnVersions.size() > 0) {
			fseek(fpStats, -1, SEEK_CUR);
		}
		
		fprintf(fpStats, "\r\n],\r\n\"Titles\": [");
		
		for (auto const &title : titles) {
			fprintf(fpStats, "\r\n{\"%08X\": %zu},", title.first, title.second);
		}
		if (titles.size() > 0) {
			fseek(fpStats, -1, SEEK_CUR);
		}
		
		fprintf(fpStats, "\r\n],\r\n\"TitleVersions\": [");
		
		for (auto const &titleVersion : titleVersions) {
			fprintf(fpStats, "\r\n{\"%016lX\": %zu},", titleVersion.first, titleVersion.second);
		}
		if (titleVersions.size() > 0) {
			fseek(fpStats, -1, SEEK_CUR);
		}
		
		fprintf(fpStats, "\r\n]\r\n}\r\n");
		
		fflush(fpStats);
		
		if (ftruncate(fileno(fpStats), ftello(fpStats)) == -1) {
			XLLN_DEBUG_LOG(XLLN_LOG_CONTEXT_XLIVELESSNESS | XLLN_LOG_LEVEL_ERROR
				, "Failed to truncate Stats File with error: %d."
				, errno
			);
		}
	}
	
	return 0;
}

int InitStats()
{
	if (!stats_file) {
		return EXIT_SUCCESS;
	}
	
	int result = EXIT_SUCCESS;
	
	pthread_attr_t thread_attr;
	pthread_attr_init(&thread_attr);
	pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_JOINABLE);
	
	pthread_create(&xlln_thread_stats_file, &thread_attr, ThreadStatsFile, stats_file);
	
	pthread_attr_destroy(&thread_attr);
	
	return result;
}

int UninitStats()
{
	if (!stats_file) {
		return EXIT_SUCCESS;
	}
	
	void *resultThread = 0;
	int result = EXIT_SUCCESS;
	
	if (result = pthread_join(xlln_thread_stats_file, &resultThread)) {
		XLLN_DEBUG_LOG(XLLN_LOG_CONTEXT_XLIVELESSNESS | XLLN_LOG_LEVEL_FATAL
			, "pthread_join failed on xlln_thread_stats_file=%d with error %d."
			, xlln_thread_stats_file
			, result
		);
		return result;
	}
	
	if (stats_file) {
		fclose(stats_file);
		stats_file = 0;
	}
	
	return result;
}
